Skip to content

feat: conversational Slack chat bots (inbox-buddy + joke-bot) + @mention wake-gate#85

Merged
khaliqgant merged 12 commits into
mainfrom
feat/inbox-buddy-email-chat-bot
Jun 22, 2026
Merged

feat: conversational Slack chat bots (inbox-buddy + joke-bot) + @mention wake-gate#85
khaliqgant merged 12 commits into
mainfrom
feat/inbox-buddy-email-chat-bot

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Jun 22, 2026

Copy link
Copy Markdown
Member

What

Lands the conversational Slack chat-bot work and supporting agent-infra hardening.

Conversational chat bots

  • inbox-buddy (existing commits on branch): Gmail conversational bot — app_mention-driven wake (webhook-driven, mount-independent), sandbox:true (lightweight delivery skips the VFS mount).
  • joke-bot (new): minimal proof bot for the same pattern — one @mention in a Slack channel → one joke reply in the right channel. Exercises the same wake/guard/dedupe plumbing end-to-end. persona.json + .workforce/ stay gitignored (generated from persona.ts).

Infra / tooling

  • .gitignore hardening: ignore opencode.json (held live relay credentials locally — never committed, now can't be), .agentworkforce/ (local CLI/handoff state), and .test-build/ (build output).
  • linear-slack: gate message.created wake on @mention so cloud only provisions a Daytona box when the agent is actually addressed (avoids sandbox-per-message waste).
  • review: useSubscription: true.
  • evals: substring log matching so enriched log lines still satisfy expectations.
  • deps: @relayfile/relay-helpers 0.3.42^0.4.1.
  • daytona-monitor: add inbox-test fixture.

Notes

  • Functional proof of joke-bot (one @mention → one correct reply, right channel) depends on cloud #2423 (Slack-forward dedupe) being deployed; redeploy + tail/verify is the follow-up step.
  • Latency caveat: sandbox:false does not skip the Daytona box (cloud#2418), so replies are ~minutes. Boxless exec is being pursued separately as a follow-up.

🤖 Generated with Claude Code


Summary by cubic

Adds two conversational Slack bots: inbox-buddy chats about Gmail with multi‑turn memory, and joke-bot proves the same @mention pattern with quick replies. Also threads hn-monitor digests under a count header and hardens its Q&A path with defensive reads.

  • New Features

    • inbox-buddy: slack.app_mention wake; reads /google-mail/threads/**; per‑thread/channel memory in ctx.memory; grounded replies; sandbox: true.
    • joke-bot: threaded @mention replies in the configured channel, supports relay inbox DM, per‑thread memory, and a scheduled “joke of the day”; sandbox: false.
    • linear-slack: gate message.created wakes on @mention to avoid provisioning a box per message.
  • Refactors

    • Persona: scope Gmail to /google-mail/threads/** (+ /google-mail/LAYOUT.md) so threads mount; avoid the dropped provider‑root glob.
    • Conversation: make transcript save best‑effort so replies don’t fail on transient memory errors.
    • Slack: normalize channel ids to bare form and strip only the leading @mention; use bare ids for replies.
    • hn-monitor: post a top‑level count header, then thread the digest with replyTo; keep seen‑idempotency, fall back to a plain digest on LLM timeout/error, fail loudly on missing Slack writebacks, and use defensive reads for stored post records (optional‑chain fields, default empty arrays, safe sort on postedAt).
    • Evals: add multi‑turn turns with shared ctx.memory; substring log matching; replyContains grounding checks; only judge the final turn’s reply; promote channel/messageId from the merged resource in multi‑turn cases.
    • Tests/Docs: preserve/restore RELAYFILE_MOUNT_ROOT in tests; clarify SLACK_CHANNEL is optional and app_mention is webhook‑driven; add “Launch Agent” buttons and README banners.
    • .gitignore: ignore opencode.json, .agentworkforce/, .test-build/.
    • review: set useSubscription: true.
    • daytona-monitor: add inbox-test fixture.
    • Deps: bump @relayfile/relay-helpers to ^0.4.1.

Written for commit 96ea19e. Summary will update on new commits.

Review in cubic

khaliqgant and others added 5 commits June 20, 2026 20:38
A Slack-channel chat agent that answers questions about recent Gmail over a
multi-turn conversation. Dogfooding forcing-function for conversational + email
threading.

- Human channel is Slack (a `slack` trigger watching one SLACK_CHANNEL), not the
  relay inbox — the relay inbox is agent-to-agent and never fires from a human
  Slack message. Mirrors the in-production linear-slack pattern. lib/slack.ts
- Reads Gmail from the relayfile VFS at /google-mail/threads/** (the real cloud
  path; the legacy /gmail adapter path is unused). lib/gmail.ts
- Conversational continuity: persists the per-conversation transcript in
  ctx.memory (workspace scope), keyed by the Slack thread/channel, and replays
  it each turn (ctx.llm.complete is stateless). lib/conversation.ts
- Email threading: resolves "that thread with X" and reasons over the full
  message list. lib/{gmail,prompt}.ts
- Replies in Slack; ignores bot/edit messages (loop guard).
- Golden tests prove multi-turn continuity + thread focus + slack gating + the
  /google-mail scope invariant; chat eval cases incl. a multi-turn case; live
  judge passes both. run-evals.mjs gains multi-turn `turns` support.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…unt)

A sandbox:false (lightweight) delivery runs without a Daytona box, so the
relayfile-mount daemon never runs and /google-mail reads come back empty. Match
the in-production linear-slack agent (sandbox:true): the box mounts the VFS and
the handler answers with ctx.llm.complete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ent wake)

Switch from the `message.created` Slack trigger to `app_mention`. `message.created`
fires only on INGESTED message records, so a stalled relayfile slack sync (the
migration) silently prevents the agent from ever waking. `app_mention` is
webhook-driven — the message arrives in the event payload — so the wake-up is
independent of the (stale) slack mount. Mirrors the in-production review-agent.
SLACK_CHANNEL is now optional (handler-side restriction, no watch-path interp).

Note: the wake-up is mount-independent, but the handler still reads the gmail
mount for context and replies via slack writeback, both of which depend on
relayfile being healthy — currently blocked by the relayfile migration
(api.relayfile.dev returns empty revisions; mount sync frozen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… agent)

End-to-end proof bot for the conversational-agent pattern: one @mention in a
Slack channel -> one joke reply in the same channel. Shares the inbox-buddy
wake/guard plumbing (app_mention-driven, correct-channel reply guard, forward
dedupe). Persona generated from persona.ts; persona.json + .workforce/ stay
gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… matching

- .gitignore: ignore opencode.json (holds live relay creds), .agentworkforce/
  (local CLI/handoff state) and .test-build/ (build output) so they can't be
  committed.
- linear-slack: gate message.created wake on @mention so cloud only provisions a
  Daytona box when the agent is actually addressed (avoids sandbox-per-message).
- review: useSubscription: true.
- evals: substring log matching so enriched log lines still match expectations.
- deps: @relayfile/relay-helpers 0.3.42 -> ^0.4.1.
- daytona-monitor: add inbox-test fixture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces two new Slack agents (inbox-buddy for multi-turn Gmail thread Q&A and joke-bot for LLM-generated jokes), each with persona configs, VFS integrations, memory persistence, and supporting lib modules. Extends the eval harness to support multi-turn test cases with shared context, and adds minor fixes to the linear-slack trigger wake-gate, review persona subscription flag, a dependency bump, and .gitignore entries.

Changes

inbox-buddy: Slack Gmail Agent

Layer / File(s) Summary
Gmail VFS types, loading, and query selection
inbox-buddy/lib/gmail.ts
Defines GmailThread/GmailThreadMessage interfaces, VFS path constants, isThreadRecord guard, recency helpers, loadRecentThreads, queryTokens/selectThreads for free-text matching, and compactThreadOverview/expandThread formatters.
Conversation transcript persistence
inbox-buddy/lib/conversation.ts
Defines ConvTurn interface and MAX_TURNS cap; implements loadConversation (recall + validate from ctx.memory), saveConversation (trim + persist), recordTurn (append + save), and renderTranscript (prompt-ready output).
Slack transport and message gating
inbox-buddy/lib/slack.ts
Defines SlackMessage/SlackPoster interfaces and defaultSlack(); adds payload normalization helpers, readSlackMessage parser, stripLeadingMention, conversationKeyForSlack, skipReason filter, and postReply with thread-aware posting.
Prompt assembly
inbox-buddy/lib/prompt.ts
Exports SYSTEM_PROMPT, BuildPromptArgs interface, buildPrompt (assembles transcript + compact thread overview + focused expansion + question), and focusedThreadIds for observability.
Agent handler and persona wiring
inbox-buddy/persona.ts, inbox-buddy/agent.ts
Persona mounts Gmail threads at /google-mail/**, scopes Slack, sets useSubscription, 60-day memory TTL, and SLACK_CHANNEL input. Agent wires Slack app_mention to handleSlackMessage: parses payload, gates by channel, loads threads and transcript, builds prompt, runs LLM with timeout and fallback, records turn, posts reply.
Tests, eval seeds, fixtures, and README
tests/inbox-buddy.test.mjs, evals/cases.jsonl, evals/seeds/gmail-thread-*.json, daytona-monitor/fixtures/inbox-test.json, inbox-buddy/README.md
Test suite covers Gmail utils, Slack parsing/gating, multi-turn continuity, thread focus, bot guard, threaded reply, VFS parsing, and persona scope invariants. Eval cases (single-turn and multi-turn) with four Gmail thread seed files, a DM inbox fixture, and README documenting design, gap fixes, and local testing.

joke-bot: Slack Joke Agent

Layer / File(s) Summary
joke-bot persona, agent, and fixture
joke-bot/persona.ts, joke-bot/agent.ts, joke-bot/fixtures/inbox-joke.json
Persona configures useSubscription, Claude Haiku model, Slack scope, SLACK_CHANNEL input, relay inbox, and 30-day memory TTL. Agent implements handleJokeOfTheDay (cron) and handleSlackMention with channel gating, LLM joke generation via ctx.llm.complete, threaded Slack posting, memory persistence, and receipt validation. DM inbox fixture included.

Eval harness multi-turn support and cross-cutting fixes

Layer / File(s) Summary
Multi-turn eval envelope builder and execution
scripts/evals/run-evals.mjs
buildEnvelopes produces one envelope per turns entry; simulate passes all envelopes to a single simulateInvocation and aggregates results; live mode loops the handler once per envelope sharing ctx/memory, accumulates outputs into replies[], and selects the last reply for chat-multiturn. logsAny switched to substring matching.
linear-slack mention gate, review persona, dep, and gitignore
linear-slack/agent.ts, review/persona.ts, package.json, .gitignore
Adds match: '@mention' wake-gate to linear-slack Slack trigger; sets useSubscription: true on review persona; bumps @relayfile/relay-helpers to ^0.4.1; adds .gitignore entries for .agentworkforce/, .test-build/, and opencode.json.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant Slack
  participant handleSlackMessage
  participant loadConversation as loadConversation<br/>(ctx.memory)
  participant loadRecentThreads as loadRecentThreads<br/>(VFS /google-mail/threads)
  participant buildPrompt
  participant LLM as ctx.llm.complete
  participant recordTurn as recordTurn<br/>(ctx.memory)

  User->>Slack: `@inbox-buddy` what happened with Alice?
  Slack->>handleSlackMessage: app_mention event
  handleSlackMessage->>loadConversation: recall transcript by conv key
  loadConversation-->>handleSlackMessage: ConvTurn[]
  handleSlackMessage->>loadRecentThreads: list threads JSON from VFS
  loadRecentThreads-->>handleSlackMessage: GmailThread[]
  handleSlackMessage->>buildPrompt: transcript + threads + question
  buildPrompt-->>handleSlackMessage: composed prompt (overview + focused expansion)
  handleSlackMessage->>LLM: complete(systemPrompt, userPrompt)
  LLM-->>handleSlackMessage: answer text
  handleSlackMessage->>recordTurn: persist updated transcript
  handleSlackMessage->>Slack: postReply (thread or channel)
  Slack-->>User: answer
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • AgentWorkforce/agents#58: Both PRs modify scripts/evals/run-evals.mjs and evals/cases.jsonl to extend how agent evaluation cases are built and executed.
  • AgentWorkforce/agents#83: Directly overlaps on joke-bot/agent.ts, joke-bot/persona.ts, joke-bot/fixtures/inbox-joke.json, and the linear-slack/agent.ts @mention trigger gate introduced in this PR.

Poem

🐇✉️ Hoppin' through email threads so grand,
Each Gmail snippet close at hand.
Slack asks a question — bunny thinks!
The transcript recalled before it blinks.
Multi-turn jokes and grounded replies,
No hallucinations — only wise! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: introducing conversational Slack bots (inbox-buddy and joke-bot) and adding a wake-gate for @mentions in linear-slack.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description comprehensively details the additions of two conversational Slack bots (inbox-buddy and joke-bot), infrastructure hardening across multiple systems, and dependency updates that directly align with all file changes in the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/inbox-buddy-email-chat-bot

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces two new conversational agents: inbox-buddy, which allows users to query their Gmail threads directly from Slack with multi-turn continuity, and joke-bot, a lightweight agent for generating jokes. It also optimizes the linear-slack agent trigger to wake only on mentions, and updates the evaluation script to support multi-turn chat scenarios. The review feedback highlights critical issues regarding Slack channel ID normalization across both new agents, where the presence of a __name suffix can cause silent failures, incorrect memory lookups, or API errors. Additionally, a regex issue in joke-bot was identified where all user mentions are stripped instead of just the leading bot mention.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread inbox-buddy/lib/slack.ts
Comment on lines +90 to +96
export function skipReason(msg: SlackMessage, boardChannel: string | undefined): string | null {
if (msg.isBot) return 'bot message';
if (msg.subtype) return `slack subtype ${msg.subtype}`;
if (boardChannel && msg.channel !== boardChannel) return 'not the chat channel';
if (!stripLeadingMention(msg.text).trim()) return 'empty message text';
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If either msg.channel or boardChannel contains the __name suffix (which is common in this platform's Slack payload/picker formats), the direct comparison msg.channel !== boardChannel will fail, causing the bot to silently ignore valid messages. Normalize both to bare IDs by splitting on __ before comparing.

export function skipReason(msg: SlackMessage, boardChannel: string | undefined): string | null {
  if (msg.isBot) return 'bot message';
  if (msg.subtype) return `slack subtype ${msg.subtype}`;
  if (boardChannel) {
    const chanId = msg.channel.split('__')[0];
    const wantId = boardChannel.split('__')[0];
    if (chanId !== wantId) return 'not the chat channel';
  }
  if (!stripLeadingMention(msg.text).trim()) return 'empty message text';
  return null;
}

Comment thread inbox-buddy/lib/slack.ts
Comment on lines +100 to +112
export async function postReply(
ctx: WorkforceCtx,
slack: SlackPoster,
msg: SlackMessage,
text: string
): Promise<void> {
const result = msg.threadTs
? await slack.reply(msg.channel, msg.threadTs, text)
: await slack.post(msg.channel, text);
if (!result?.ts) {
ctx.log?.('warn', 'inbox-buddy.reply.no-receipt', { channel: msg.channel, threaded: Boolean(msg.threadTs) });
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When posting a reply, using msg.channel directly might fail if it contains the __name suffix, as the Slack API/client expects a bare channel ID. Normalize the channel ID to its bare form before calling slack.reply or slack.post.

Suggested change
export async function postReply(
ctx: WorkforceCtx,
slack: SlackPoster,
msg: SlackMessage,
text: string
): Promise<void> {
const result = msg.threadTs
? await slack.reply(msg.channel, msg.threadTs, text)
: await slack.post(msg.channel, text);
if (!result?.ts) {
ctx.log?.('warn', 'inbox-buddy.reply.no-receipt', { channel: msg.channel, threaded: Boolean(msg.threadTs) });
}
}
export async function postReply(
ctx: WorkforceCtx,
slack: SlackPoster,
msg: SlackMessage,
text: string
): Promise<void> {
const chanId = msg.channel.split('__')[0];
const result = msg.threadTs
? await slack.reply(chanId, msg.threadTs, text)
: await slack.post(chanId, text);
if (!result?.ts) {
ctx.log?.('warn', 'inbox-buddy.reply.no-receipt', { channel: msg.channel, threaded: Boolean(msg.threadTs) });
}
}

Comment thread inbox-buddy/lib/slack.ts
Comment on lines +81 to +83
export function conversationKeyForSlack(msg: SlackMessage): string {
return msg.threadTs ? `${msg.channel}:${msg.threadTs}` : msg.channel;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Normalize the channel ID to its bare form (splitting on __) when generating the conversation key to ensure consistency in memory lookups regardless of how the channel ID is formatted in the payload.

Suggested change
export function conversationKeyForSlack(msg: SlackMessage): string {
return msg.threadTs ? `${msg.channel}:${msg.threadTs}` : msg.channel;
}
export function conversationKeyForSlack(msg: SlackMessage): string {
const chanId = msg.channel.split('__')[0];
return msg.threadTs ? `${chanId}:${msg.threadTs}` : chanId;
}

Comment thread joke-bot/agent.ts Outdated
Comment on lines +145 to +154
const want = input(ctx, 'SLACK_CHANNEL');
const chanId = channel.split('__')[0];
if (!want) {
ctx.log?.('warn', 'joke-bot.slack-no-channel', { reason: 'SLACK_CHANNEL not set; failing closed' });
return;
}
if (chanId !== want) {
ctx.log?.('info', 'joke-bot.slack-wrong-channel', { channel: chanId, want });
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If SLACK_CHANNEL is configured with the id__name format (e.g., from a channel picker), the comparison chanId !== want will fail because chanId is normalized to a bare ID while want is not. Normalize want as well by splitting on __.

Suggested change
const want = input(ctx, 'SLACK_CHANNEL');
const chanId = channel.split('__')[0];
if (!want) {
ctx.log?.('warn', 'joke-bot.slack-no-channel', { reason: 'SLACK_CHANNEL not set; failing closed' });
return;
}
if (chanId !== want) {
ctx.log?.('info', 'joke-bot.slack-wrong-channel', { channel: chanId, want });
return;
}
const want = input(ctx, 'SLACK_CHANNEL')?.split('__')[0];
const chanId = channel.split('__')[0];
if (!want) {
ctx.log?.('warn', 'joke-bot.slack-no-channel', { reason: 'SLACK_CHANNEL not set; failing closed' });
return;
}
if (chanId !== want) {
ctx.log?.('info', 'joke-bot.slack-wrong-channel', { channel: chanId, want });
return;
}

Comment thread joke-bot/agent.ts Outdated
Comment on lines +171 to +176
const tag = `joke-convo:slack:${channel}:${threadTs}`;
const reply = await joke(ctx, buildPrompt(await recall(ctx, tag), question));
const result = await slackClient({ writebackTimeoutMs: 15_000 }).reply(channel, threadTs, reply);
if (!result?.ts) throw new Error('Slack reply returned no receipt ts');
await remember(ctx, tag, question, reply);
ctx.log?.('info', 'joke-bot.slack-replied', { channel, threadTs, chars: reply.length });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use the normalized chanId instead of channel when calling slack.reply to prevent potential Slack API errors if the channel ID contains the __name suffix.

Suggested change
const tag = `joke-convo:slack:${channel}:${threadTs}`;
const reply = await joke(ctx, buildPrompt(await recall(ctx, tag), question));
const result = await slackClient({ writebackTimeoutMs: 15_000 }).reply(channel, threadTs, reply);
if (!result?.ts) throw new Error('Slack reply returned no receipt ts');
await remember(ctx, tag, question, reply);
ctx.log?.('info', 'joke-bot.slack-replied', { channel, threadTs, chars: reply.length });
const tag = `joke-convo:slack:${chanId}:${threadTs}`;
const reply = await joke(ctx, buildPrompt(await recall(ctx, tag), question));
const result = await slackClient({ writebackTimeoutMs: 15_000 }).reply(chanId, threadTs, reply);
if (!result?.ts) throw new Error('Slack reply returned no receipt ts');

Comment thread joke-bot/agent.ts
Comment on lines +180 to +190
async function handleJokeOfTheDay(ctx: WorkforceCtx): Promise<void> {
const channel = input(ctx, 'SLACK_CHANNEL');
if (!channel) {
ctx.log?.('warn', 'joke-bot.no-channel', { reason: 'SLACK_CHANNEL not set; skipping joke of the day' });
return;
}
const reply = await joke(ctx, 'Give me one short, original "joke of the day" about recent tech / pop-culture / current events.');
const result = await slackClient({ writebackTimeoutMs: 15_000 }).post(channel, `🃏 Joke of the day:\n${reply}`);
if (!result?.ts) throw new Error('Slack post returned no receipt ts for joke of the day');
ctx.log?.('info', 'joke-bot.jotd-posted', { channel });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Normalize the channel ID to its bare form before calling slack.post to ensure compatibility with the Slack API if SLACK_CHANNEL is configured with the id__name format.

Suggested change
async function handleJokeOfTheDay(ctx: WorkforceCtx): Promise<void> {
const channel = input(ctx, 'SLACK_CHANNEL');
if (!channel) {
ctx.log?.('warn', 'joke-bot.no-channel', { reason: 'SLACK_CHANNEL not set; skipping joke of the day' });
return;
}
const reply = await joke(ctx, 'Give me one short, original "joke of the day" about recent tech / pop-culture / current events.');
const result = await slackClient({ writebackTimeoutMs: 15_000 }).post(channel, `🃏 Joke of the day:\n${reply}`);
if (!result?.ts) throw new Error('Slack post returned no receipt ts for joke of the day');
ctx.log?.('info', 'joke-bot.jotd-posted', { channel });
}
async function handleJokeOfTheDay(ctx: WorkforceCtx): Promise<void> {
const channel = input(ctx, 'SLACK_CHANNEL');
if (!channel) {
ctx.log?.('warn', 'joke-bot.no-channel', { reason: 'SLACK_CHANNEL not set; skipping joke of the day' });
return;
}
const chanId = channel.split('__')[0];
const reply = await joke(ctx, 'Give me one short, original "joke of the day" about recent tech / pop-culture / current events.');
const result = await slackClient({ writebackTimeoutMs: 15_000 }).post(chanId, `🃏 Joke of the day:\n${reply}`);
if (!result?.ts) throw new Error('Slack post returned no receipt ts for joke of the day');
ctx.log?.('info', 'joke-bot.jotd-posted', { channel: chanId });
}

Comment thread joke-bot/agent.ts Outdated
Comment on lines +111 to +126
const channel = input(ctx, 'SLACK_CHANNEL');
if (!channel) {
ctx.log?.('warn', 'joke-bot.no-channel', { reason: 'SLACK_CHANNEL not set; cannot reply' });
return;
}
const question = await readQuestion(event);
if (!question) {
ctx.log?.('info', 'joke-bot.empty', { reason: 'no text in message; skipping' });
return;
}
const tag = `joke-convo:${channel}`;
const reply = await joke(ctx, buildPrompt(await recall(ctx, tag), question));
const result = await slackClient({ writebackTimeoutMs: 15_000 }).post(channel, reply);
if (!result?.ts) throw new Error('Slack post returned no receipt ts');
await remember(ctx, tag, question, reply);
ctx.log?.('info', 'joke-bot.replied', { channel, surface: 'relay', chars: reply.length });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Normalize the channel ID to its bare form before using it for memory tags and posting to Slack to ensure consistency and prevent API errors if SLACK_CHANNEL contains the __name suffix.

    const channel = input(ctx, 'SLACK_CHANNEL');
    if (!channel) {
      ctx.log?.('warn', 'joke-bot.no-channel', { reason: 'SLACK_CHANNEL not set; cannot reply' });
      return;
    }
    const chanId = channel.split('__')[0];
    const question = await readQuestion(event);
    if (!question) {
      ctx.log?.('info', 'joke-bot.empty', { reason: 'no text in message; skipping' });
      return;
    }
    const tag = `joke-convo:${chanId}`;
    const reply = await joke(ctx, buildPrompt(await recall(ctx, tag), question));
    const result = await slackClient({ writebackTimeoutMs: 15_000 }).post(chanId, reply);
    if (!result?.ts) throw new Error('Slack post returned no receipt ts');
    await remember(ctx, tag, question, reply);
    ctx.log?.('info', 'joke-bot.replied', { channel: chanId, surface: 'relay', chars: reply.length });

Comment thread joke-bot/agent.ts Outdated
return;
}
const threadTs = typeof data.thread_ts === 'string' && data.thread_ts ? data.thread_ts : ts;
const question = rawText.replace(/<@[^>]+>/g, '').trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using a global regex /<@[^>]+>/g will strip all user mentions from the message, including mentions of other users (e.g., @joke-bot tell a joke to @alice becomes tell a joke to). Only strip the leading mention to preserve other mentions in the message.

Suggested change
const question = rawText.replace(/<@[^>]+>/g, '').trim();
const question = rawText.replace(/^\s*<@[^>]+>\s*/, '').trim();

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/AgentWorkforce/agents/issues/comments/4766528153","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- review_stack_entry_start -->\n\n[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/AgentWorkforce/agents/pull/85?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)\n\n<!-- review_stack_entry_end -->\n<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->\n\n> [!NOTE]\n> Currently processing new changes in this PR. This may take a few minutes, please wait...\n> \n> <details>\n> <summary>⚙️ Run configuration</summary>\n> \n> **Configuration used**: Organization UI\n> \n> **Review profile**: CHILL\n> \n> **Plan**: Pro Plus\n> \n> **Run ID**: `83a320b1-eac5-4244-861a-a463124f7bcc`\n> \n> </details>\n> \n> <details>\n> <summary>📥 Commits</summary>\n> \n> Reviewing files that changed from the base of the PR and between 873e0c56f1b7de94a1a701f467a06b12a29ac2fa and 3cbad0dead46ce23cf151d0ce2a141a5da548692.\n> \n> </details>\n> \n> <details>\n> <summary>⛔ Files ignored due to path filters (1)</summary>\n> \n> * `package-lock.json` is excluded by `!**/package-lock.json`\n> \n> </details>\n> \n> <details>\n> <summary>📒 Files selected for processing (22)</summary>\n> \n> * `.gitignore`\n> * `daytona-monitor/fixtures/inbox-test.json`\n> * `evals/cases.jsonl`\n> * `evals/seeds/gmail-thread-alice-export.json`\n> * `evals/seeds/gmail-thread-bob-lunch.json`\n> * `evals/seeds/gmail-thread-github-pr.json`\n> * `evals/seeds/gmail-thread-newsletter.json`\n> * `inbox-buddy/README.md`\n> * `inbox-buddy/agent.ts`\n> * `inbox-buddy/lib/conversation.ts`\n> * `inbox-buddy/lib/gmail.ts`\n> * `inbox-buddy/lib/prompt.ts`\n> * `inbox-buddy/lib/slack.ts`\n> * `inbox-buddy/persona.ts`\n> * `joke-bot/agent.ts`\n> * `joke-bot/fixtures/inbox-joke.json`\n> * `joke-bot/persona.ts`\n> * `linear-slack/agent.ts`\n> * `package.json`\n> * `review/persona.ts`\n> * `scripts/evals/run-evals.mjs`\n> * `tests/inbox-buddy.test.mjs`\n> \n> </details>\n> \n> ```ascii\n>  ________________________________________________________________________\n> < OpenAI said I could be anything I wanted, so I became a code reviewer. >\n>  ------------------------------------------------------------------------\n>   \\\n>    \\   (\\__/)\n>        (•ㅅ•)\n>        /   づ\n> ```\n\n<!-- end of auto-generated comment: review in progress by coderabbit.ai -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>📝 Generate docstrings</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> Create stacked PR\n- [ ] <!-- {\"checkboxId\": \"3e1879ae-f29b-4d0d-8e06-d12b7ba33d98\"} --> Commit on current branch\n\n</details>\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `feat/inbox-buddy-email-chat-bot`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=AgentWorkforce/agents&utm_content=85)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->"},"request":{"retryCount":3,"signal":{},"retries":3,"retryAfter":16}}}

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment thread joke-bot/persona.ts
// Slack is the reply surface (writeback to /slack/channels/{id}/messages), so
// scope channels. An unscoped slack mount would make post() a silent no-op.
integrations: {
slack: { scope: { channels: '/slack/channels/**' } }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 joke-bot Slack scope uses wrong key channels instead of paths, likely making writebacks silently fail

The joke-bot/persona.ts Slack integration scope uses { channels: '/slack/channels/**' }, but every other agent in the repo (9+ agents) consistently uses { paths: '/slack/channels/**' } for the Slack scope key. The comment on lines 66-67 explicitly warns: "An unscoped slack mount would make post() a silent no-op." Using the unrecognized key channels instead of paths likely means the Slack mount is effectively unscoped, causing slackClient().post() and .reply() to silently drop all messages — the bot generates a joke but the reply never reaches Slack.

All other agents use `paths` for Slack scope

vendor-monitor/persona.ts:17, linear-slack/persona.ts:41, hn-monitor/persona.ts:19, spotify-releases/persona.ts:18, neon-monitor/persona.ts:63, daytona-monitor/persona.ts:35, review/persona.ts:35, inbox-buddy/persona.ts:52, repo-hygiene/persona.ts:24, gcp-watcher/persona.ts:45 — all use scope: { paths: '/slack/channels/**' }.

Suggested change
slack: { scope: { channels: '/slack/channels/**' } }
slack: { scope: { paths: '/slack/channels/**' } }
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3cbad0dead

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread inbox-buddy/persona.ts Outdated
// Gmail threads materialize under /google-mail (provider id `google-mail`).
// `/google-mail/**` mounts the threads + LAYOUT.md the handler reads. An
// unscoped mirror (or a `/gmail/**` scope) would mount nothing.
'google-mail': { scope: { paths: '/google-mail/**' } },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope Gmail below the provider root

This grants the provider-root glob /google-mail/**. The existing linear-slack/persona.ts notes that cloud's mirror drops provider-root globs via isProviderRootPath, so inbox-buddy's sandbox can be deployed without the /google-mail/threads files it reads; loadRecentThreads() then catches the failure and returns [], making the bot claim no Gmail is visible. Scope the concrete subtree the handler needs, such as /google-mail/threads/** (and LAYOUT.md separately if needed).

Useful? React with 👍 / 👎.

Comment thread tests/inbox-buddy.test.mjs Outdated
// ── persona config invariant (§1 scope trap) ──────────────────────────────────

test('compiled persona scopes the REAL gmail mount (/google-mail, not legacy /gmail) + slack', async () => {
const persona = JSON.parse(readFileSync(path.join(HERE, '..', 'inbox-buddy', 'persona.json'), 'utf8'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Read the compiled persona from .test-build

In a fresh checkout npm test only runs tsc --outDir .test-build; it does not generate the ignored inbox-buddy/persona.json file. This assertion therefore fails unless the developer happened to run npm run compile beforehand, so the new inbox-buddy test suite is not reproducible from the documented test command. Import .test-build/inbox-buddy/persona.js here, or have the test setup compile persona JSON first.

Useful? React with 👍 / 👎.

…review)

Addresses Gemini review feedback on PR #85:
- joke-bot/inbox-buddy: normalize Slack channel ids to bare form (strip the
  `__name` suffix the platform appends in some payloads) before comparing the
  channel guard, keying memory, and calling the Slack API. Added a shared
  bareChannelId() helper in inbox-buddy/lib/slack.ts. Safe no-op on bare ids.
- joke-bot: strip ONLY the leading bot mention (`/^\s*<@[^>]+>\s*/`) instead of a
  global `/<@[^>]+>/g`, so other mentions in the message text are preserved.

Also fixes a pre-existing typecheck break that stopped `npm test` from running:
inbox-buddy's app_mention handler event type isn't in the runtime's exported
event unions, so cast across that type-defs gap at the call site. Typecheck now
clean; full suite (142 tests) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@khaliqgant

Copy link
Copy Markdown
Member Author

Addressed the review feedback in 4c5afcc:

  • Channel id__name normalization (Gemini, multiple comments on joke-bot/agent.ts + inbox-buddy/lib/slack.ts): added a shared bareChannelId() helper in inbox-buddy/lib/slack.ts and normalize the channel to its bare form before the channel guard, memory keys, and Slack API calls. In joke-bot, the guard's want (SLACK_CHANNEL) is now normalized alongside the event channel, and the bare id is used for the memory tag + slack.reply/post. Safe no-op on already-bare ids.
  • Global mention strip (joke-bot/agent.ts:165): now strips only the leading bot mention (/^\s*<@[^>]+>\s*/) so other mentions in the text are preserved.

Also fixed a pre-existing typecheck break (unrelated to the review) that was stopping npm test from running at all: inbox-buddy's app_mention handler event type isn't in the runtime's exported event unions, so I cast across that type-defs gap at the call site with a comment. Typecheck is now clean and the full suite (142 tests) is green.

@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #85 in AgentWorkforce/agents.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

pr-reviewer could not complete review for #85 in AgentWorkforce/agents.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (1)
joke-bot/persona.ts (1)

14-16: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Align persona documentation/config comments with the actual generation path.

Lines 14-16 and 40-42 describe harness-based joke generation, but the handler currently uses ctx.llm.complete. This mismatch will mislead debugging and ops expectations.

Also applies to: 40-43

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@joke-bot/persona.ts` around lines 14 - 16, The comments at lines 14-16 and
40-42 in persona.ts incorrectly document the joke generation path as using
ctx.harness.run, but the actual handler implementation uses ctx.llm.complete
instead. Update these comments to accurately reflect that jokes are generated
via ctx.llm.complete, removing the misleading reference to ctx.harness.run and
the incorrect description about testing different provider behaviors through
harness switching. This will ensure the documentation aligns with the actual
implementation and prevents future debugging confusion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@evals/cases.jsonl`:
- Around line 12-13: The test cases inbox-buddy.chat and
inbox-buddy.chat-multiturn currently only verify success status, side effects,
and log output in their expect blocks, but do not assert anything about the
actual Slack reply text content. This means a hallucinated or incorrect reply
would still pass the test. Add a machine-checked assertion field (such as
replyContains or similar validation mechanism) to the expect object in both test
cases that verifies the Slack reply text includes the specific facts mentioned
in each case's rubric: for inbox-buddy.chat, assert the reply mentions "final
numbers by Friday" and "finance"; for inbox-buddy.chat-multiturn, assert the
second turn's reply mentions "finance@acme.com" and "Friday" to prove context
was retained from the first turn.

In `@evals/seeds/gmail-thread-newsletter.json`:
- Around line 13-17: The internalDate field (line 13) and date field (line 17)
in the gmail-thread-newsletter.json seed file represent different timestamps for
the same message, which can cause incorrect ordering in tests that depend on
internalDate. Convert the human-readable date string "Wed, 03 Jun 2026 12:00:00
-0700" to its corresponding Unix timestamp in milliseconds and update the
internalDate value to match this converted timestamp, ensuring both fields
represent the exact same point in time.

In `@inbox-buddy/lib/conversation.ts`:
- Around line 57-62: The saveConversation function currently allows errors from
ctx.memory.save to propagate and abort the chat flow. Wrap the ctx.memory.save
call in a try-catch block in the saveConversation function to catch any errors,
log them for visibility, and then return gracefully without throwing. This makes
the transcript persistence best-effort rather than request-fatal, allowing the
chat flow to continue even if memory persistence temporarily fails, which aligns
with the tolerant error handling pattern already used in loadConversation.

In `@inbox-buddy/README.md`:
- Around line 23-26: The README documentation at lines 23-26 and 53-56 indicates
that SLACK_CHANNEL is a required parameter used for trigger-path interpolation,
but the actual implementation treats it as an optional parameter that is only
used when explicitly set and gated within the handler logic. Update the README
to clarify that SLACK_CHANNEL is optional rather than required, remove the
implication about trigger-path interpolation if it is not actually used in that
manner, and explain that the parameter is conditionally used by the
persona/handler when provided.
- Around line 7-12: The fenced code block containing the conversation example
(starting with "You → `#your-channel`: What's the latest on the Q3 export thread
with Alice?") is missing a language identifier on the opening fence, which
violates the MD040 markdown rule. Add the language identifier "text" immediately
after the opening triple backticks (```) to specify the content type and resolve
the linting error.

In `@joke-bot/agent.ts`:
- Around line 146-147: The channel ID is normalized to chanId by extracting the
first part before the double underscore at lines 146-147, but the raw channel
variable is still being used in the memory tag and Slack reply operations at
lines 171 and 173. Replace all uses of the raw channel variable with the
normalized chanId variable in both the memory tag operation and the reply call
to ensure consistent channel ID handling throughout the function.
- Around line 121-122: The memory key tag on line 121 uses only the channel to
create the relay memory key, causing all conversations within that channel to
share the same memory stream and potentially leak information between users.
Modify the tag construction to include a unique conversation or user identifier
in addition to the channel variable so that each conversation has its isolated
memory. This ensures the recall function on line 122 retrieves only the relevant
conversation history for that specific user or DM thread, not shared workspace
memory.
- Around line 51-57: The readQuestion function is missing a check for
resource.text, which is where relay message payloads store the text field.
Currently the function only checks data?.text and nested.text, causing relay
messages to be treated as empty strings. Add resource?.text as a fallback option
in the text extraction logic after the nested.text check so that the function
can properly extract text from relay event payloads where data is absent but
resource.text is available.
- Around line 160-166: The regex pattern /<@[^>]+>/ in the mention gate check is
too broad as it matches any user mention in the message, not just bot
invocations. Replace this pattern to specifically match either a leading
invocation token at the start of the message or the bot's specific mention ID to
ensure the bot only responds when explicitly invoked, not when other users are
mentioned in unrelated conversations.

In `@scripts/evals/run-evals.mjs`:
- Around line 270-278: The issue is that when the final turn in a multi-turn
conversation produces no model output, the eval system silently selects the last
available reply from a previous turn instead of detecting this failure. To fix
this, modify the logic where `replies[replies.length - 1]` is selected (around
lines 341-344) to validate that the selected reply actually comes from the final
turn and that no turns were skipped due to missing output. Track which turn each
reply in the replies array corresponds to, or verify that the replies array
length matches the expected number of turns executed, so if the final turn
produced no output, the eval properly detects and reports this failure instead
of silently reusing a prior turn's result.
- Around line 95-101: In the multi-turn scenario where env.resource is merged
(line 95), the subsequent checks for promoting channel and messageId (lines
96-97) only look at the turn object. After the merge, also check the resulting
env.resource object itself for channel and messageId properties and promote them
to top-level env fields if they exist there. This ensures that channel/messageId
values carried in the fixture.resource are properly promoted even when omitted
from individual turns.
- Around line 328-330: The RELAYFILE_MOUNT_ROOT and WORKSPACE_ROOT environment
variables are being set directly on process.env at lines 328-329 without
restoration, causing these values to leak into subsequent test cases. Instead of
modifying process.env directly, save the original values of these environment
variables before the withCaseEnv call, then restore them in a finally block
after the async operation completes to ensure proper cleanup between test cases.

In `@tests/inbox-buddy.test.mjs`:
- Around line 189-225: The test in the multi-turn function currently deletes
process.env.RELAYFILE_MOUNT_ROOT in the finally block without restoring any
previously existing value. Save the original value of
process.env.RELAYFILE_MOUNT_ROOT before assigning the mount value (before
entering the try block), then in the finally block restore the saved original
value instead of using delete, so that if the env var was set before the test
ran, that value is preserved for subsequent tests.

---

Nitpick comments:
In `@joke-bot/persona.ts`:
- Around line 14-16: The comments at lines 14-16 and 40-42 in persona.ts
incorrectly document the joke generation path as using ctx.harness.run, but the
actual handler implementation uses ctx.llm.complete instead. Update these
comments to accurately reflect that jokes are generated via ctx.llm.complete,
removing the misleading reference to ctx.harness.run and the incorrect
description about testing different provider behaviors through harness
switching. This will ensure the documentation aligns with the actual
implementation and prevents future debugging confusion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 83a320b1-eac5-4244-861a-a463124f7bcc

📥 Commits

Reviewing files that changed from the base of the PR and between 873e0c5 and 3cbad0d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (22)
  • .gitignore
  • daytona-monitor/fixtures/inbox-test.json
  • evals/cases.jsonl
  • evals/seeds/gmail-thread-alice-export.json
  • evals/seeds/gmail-thread-bob-lunch.json
  • evals/seeds/gmail-thread-github-pr.json
  • evals/seeds/gmail-thread-newsletter.json
  • inbox-buddy/README.md
  • inbox-buddy/agent.ts
  • inbox-buddy/lib/conversation.ts
  • inbox-buddy/lib/gmail.ts
  • inbox-buddy/lib/prompt.ts
  • inbox-buddy/lib/slack.ts
  • inbox-buddy/persona.ts
  • joke-bot/agent.ts
  • joke-bot/fixtures/inbox-joke.json
  • joke-bot/persona.ts
  • linear-slack/agent.ts
  • package.json
  • review/persona.ts
  • scripts/evals/run-evals.mjs
  • tests/inbox-buddy.test.mjs

Comment thread evals/cases.jsonl Outdated
Comment thread evals/seeds/gmail-thread-newsletter.json Outdated
Comment thread inbox-buddy/lib/conversation.ts
Comment thread inbox-buddy/README.md Outdated
Comment thread inbox-buddy/README.md
Comment thread joke-bot/agent.ts
Comment thread scripts/evals/run-evals.mjs
Comment thread scripts/evals/run-evals.mjs
Comment on lines 328 to +330
process.env.RELAYFILE_MOUNT_ROOT = mount;
process.env.WORKSPACE_ROOT = mount;
await withCaseEnv(personaSpec, testCase.inputs ?? {}, {}, () => handler(ctx, event));
await withCaseEnv(personaSpec, testCase.inputs ?? {}, {}, async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid leaking mount env vars across test cases.

Lines 328-329 set process env directly and never restore previous values. In a multi-case run, that can bleed mount state across cases.

Suggested fix
-    process.env.RELAYFILE_MOUNT_ROOT = mount;
-    process.env.WORKSPACE_ROOT = mount;
-    await withCaseEnv(personaSpec, testCase.inputs ?? {}, {}, async () => {
+    await withCaseEnv(
+      personaSpec,
+      testCase.inputs ?? {},
+      { RELAYFILE_MOUNT_ROOT: mount, WORKSPACE_ROOT: mount },
+      async () => {
       for (let i = 0; i < events.length; i++) {
         const event = events[i];
         if (!event) throw new Error(`envelopeToAgentEvent returned null for turn ${i} (unsupported envelope)`);
         await handler(ctx, event);
       }
-    });
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
process.env.RELAYFILE_MOUNT_ROOT = mount;
process.env.WORKSPACE_ROOT = mount;
await withCaseEnv(personaSpec, testCase.inputs ?? {}, {}, () => handler(ctx, event));
await withCaseEnv(personaSpec, testCase.inputs ?? {}, {}, async () => {
await withCaseEnv(
personaSpec,
testCase.inputs ?? {},
{ RELAYFILE_MOUNT_ROOT: mount, WORKSPACE_ROOT: mount },
async () => {
for (let i = 0; i < events.length; i++) {
const event = events[i];
if (!event) throw new Error(`envelopeToAgentEvent returned null for turn ${i} (unsupported envelope)`);
await handler(ctx, event);
}
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/evals/run-evals.mjs` around lines 328 - 330, The RELAYFILE_MOUNT_ROOT
and WORKSPACE_ROOT environment variables are being set directly on process.env
at lines 328-329 without restoration, causing these values to leak into
subsequent test cases. Instead of modifying process.env directly, save the
original values of these environment variables before the withCaseEnv call, then
restore them in a finally block after the async operation completes to ensure
proper cleanup between test cases.

Comment thread tests/inbox-buddy.test.mjs
@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #85 in AgentWorkforce/agents.
The review harness exited with code 1.
No review was posted; this needs operator attention.

1 similar comment
@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #85 in AgentWorkforce/agents.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

pr-reviewer could not complete review for #85 in AgentWorkforce/agents.
The review harness exited with code 1.
No review was posted; this needs operator attention.

khaliqgant and others added 3 commits June 22, 2026 11:10
joke-bot, the linear-slack @mention gate, and the relay-helpers ^0.4.1 bump
already landed on main via #83/#84. Resolve the add/add conflicts in favor of
this branch (identical content for all except joke-bot/agent.ts, where this
branch additionally carries the PR #85 review fixes: channel id normalization +
leading-only mention strip). Net-new here remains inbox-buddy + those fixes.
…eval rigor)

- persona: scope the concrete /google-mail/threads/** subtree (+ LAYOUT.md)
  instead of the provider-root glob /google-mail/**. Cloud's mount drops
  provider-root globs (isProviderRootPath), so /google-mail/** mirrored nothing
  and the bot reported no Gmail. Matches the linear-slack precedent. (Codex P1)
- conversation: make saveConversation best-effort (log + continue) so a transient
  ctx.memory.save outage degrades continuity instead of aborting the reply.
  (CodeRabbit Major)
- test: read the tsc-compiled persona module from .test-build instead of the
  gitignored persona.json, so the persona-scope test is reproducible from
  `npm test` alone; assertions updated for the new scope shape. (Codex P2)
- evals: add a machine-checked `replyContains` assertion (case-insensitive,
  enforced only when a real reply exists / live runs) and grounded-fact
  assertions to the two inbox-buddy chat cases, so a hallucinated reply fails
  without the LLM judge. (CodeRabbit Major)
- seed: align gmail-thread-newsletter internalDate with its date timestamp.
- README: add a language to the example fenced block.

Typecheck clean; full suite 142/142 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…accuracy)

- run-evals: promote channel/messageId from the MERGED resource so a multi-turn
  turn that omits them inherits from fixture.resource (CodeRabbit Major).
- run-evals: only judge a reply the FINAL turn actually produced — return null
  instead of silently reusing an earlier turn's output, so a failed last turn
  isn't masked (CodeRabbit Major).
- tests: save/restore RELAYFILE_MOUNT_ROOT instead of deleting it, so a
  runner-set value doesn't leak away between cases (CodeRabbit Minor).
- README: SLACK_CHANNEL is optional and app_mention is webhook-driven (not
  interpolated into a watch path) — table was out of sync (CodeRabbit Minor).

Typecheck clean; full suite 142/142 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@khaliqgant

Copy link
Copy Markdown
Member Author

Conflicts resolved (merged main — joke-bot, the linear-slack @mention gate, and the relay-helpers bump already landed via #83/#84; kept this branch's superset incl. the joke-bot review fixes). PR is now mergeable.

Review feedback addressed across the last two pushes:

Codex

  • P1 — Gmail scope: persona now scopes /google-mail/threads/** + /google-mail/LAYOUT.md instead of the provider-root glob /google-mail/** (dropped by isProviderRootPath → empty mount → bot saw no Gmail). Matches the linear-slack precedent.
  • P2 — test reproducibility: the persona-scope test now imports the tsc-compiled .test-build/inbox-buddy/persona.js instead of the gitignored persona.json, so it runs from npm test alone.

CodeRabbit

  • Major — transcript persistence: saveConversation is now best-effort (log + continue) so a transient memory outage degrades continuity instead of aborting the reply.
  • Major — eval grounding: added a machine-checked replyContains assertion (case-insensitive; enforced only when a real reply exists) + grounded-fact assertions on both inbox-buddy chat cases, so a hallucinated reply fails without the LLM judge.
  • Major — multi-turn routing: channel/messageId now promoted from the merged resource (turn inherits from fixture.resource).
  • Major — multi-turn reply selection: only judge a reply the final turn actually produced (return null instead of reusing an earlier turn's).
  • Minor: test env RELAYFILE_MOUNT_ROOT save/restore instead of delete; README SLACK_CHANNEL corrected to optional/webhook-driven; newsletter seed internalDate aligned with its date; README fence given a language.

Gemini (slack.ts channel normalization, lines 94/107/124): already addressed in 4c5afcc via the shared bareChannelId() helper applied in conversationKeyForSlack, skipReason, and postReply — those threads are stale re-posts on shifted line numbers.

Also added the generated agent graphics (avatar/card/banner) for the four agents that were missing them: gcp-watcher, inbox-buddy, joke-bot, neon-monitor.

Typecheck clean; full suite 142/142 green.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

Review of PR #85feat/inbox-buddy-email-chat-bot

Summary

This PR adds two new conversational agents (inbox-buddy, joke-bot), Gmail thread seeds + eval cases, multi-turn support in the eval harness, a match: '@mention' wake-gate on linear-slack, useSubscription: true on review, and a @relayfile/relay-helpers bump (0.3.42^0.4.1). I traced the diff across callers, types, tests, config, and the eval harness, installed deps, and ran the canonical build/test/eval commands.

What I verified (against the current checkout)

  • npm run typecheck (tsc --noEmit): clean, exit 0 — all new TS files typecheck, including the event as unknown as AgentEvent cast in inbox-buddy/agent.ts.
  • node scripts/evals/run-evals.mjs (full simulate): 13/13 pass, including inbox-buddy.chat, inbox-buddy.chat-multiturn, and every pre-existing single-turn case — the multi-turn refactor of scripts/evals/run-evals.mjs (buildEnvelopes, aggregated sims, last-turn judging, substring logsAny) did not regress existing agents.
  • package-lock.json is consistent with package.json (npm ci succeeded; the dep bump resolves cleanly).
  • Inbox-buddy unit logic (gmail/prompt/conversation/slack helpers, multi-turn continuity, loop guard, threaded reply) all pass once the two environment artifacts below are accounted for.

Required finding (CI failure — needs human decision)

tests/inbox-buddy.test.mjs:296 — "compiled persona scopes the REAL gmail mount…" fails under the canonical npm test.

  • Root cause: that test does readFileSync('inbox-buddy/persona.json') (test line 1879 in the diff). persona.json is gitignored (*/persona.json) and is produced only by npm run compile (agentworkforce persona compile). The npm test script (package.json:9) runs tsc … && node --test … with no compile step, so the file is absent → ENOENT → fail. Reproduced on a clean checkout: full npm test = 141 pass / 1 fail, and this is the only real failure.
  • This is not a logic bug in the agent — npm run evals (which does compile) passes both inbox-buddy cases.
  • I did not edit the test (modifying a PR-authored test is a human decision, and the fix is a judgment call). Two valid fixes, author's choice:
    1. Match the repo convention used by tests/review-agent.test.mjs:591: import the tsc-built persona import('../.test-build/inbox-buddy/persona.js') and run parseIntegrations on it, instead of reading persona.json. I verified the compiled persona.js carries the identical scopes the test asserts (google-mail: /google-mail/**, slack: /slack/channels/**), so this keeps every assertion intact and weakens nothing.
    2. Add a pretest/compile step to npm test (preferred if the author specifically wants the test to exercise the real agentworkforce compiler output rather than tsc's).

Environment artifacts (NOT PR defects — will pass in real CI)

Two other inbox-buddy failures I saw initially are caused by env vars this review sandbox injects, not by the code:

  • WORKFORCE_SANDBOX_ROOT=/home/daytona/workspace makes resolveMountRoot({}) (adapter-core precedence) ignore the test's RELAYFILE_MOUNT_ROOT, so loadRecentThreads reads 0 threads → "loads threads…" fails. With WORKFORCE_SANDBOX_ROOT unset, it passes.
  • SLACK_CHANNEL=C0ALQ06AAUT (leaked from the harness WORKFORCE_AGENT_CONTEXT) makes the channel guard skip the test messages → multi-turn / threaded-reply skip. With it unset, both pass. (This same leak fails the unrelated pre-existing tests/hn-monitor.test.mjs "handleInboxMessage…" test — confirmed failing identically on base SHA 873e0c5, so it predates and is outside this PR.)

Addressed comments

There were no bot or human review comments in the provided PR metadata (.workforce/context.json carries only PR metadata; no review threads). Nothing to validate or reconcile.

Advisory Notes

  • joke-bot/persona.ts:14-15, 41 — contradictory comments. They state the joke is "generated via ctx.harness.run (NOT ctx.llm.complete)" and "The joke is produced by ctx.harness.run", but the handler (joke-bot/agent.ts:46) and the same file's lines 29/36 use ctx.llm.complete. Doc-only, no runtime effect; the harness field does not drive joke generation on the llm.complete path. Left unchanged because it touches design-intent prose (you may intend a future harness.run switch) — please reconcile the comments to the actual ctx.llm.complete path or clarify the intended direction.

Notes on safety-sensitive areas (left as-is, correctly)

  • inbox-buddy/agent.ts withTimeout/fallbackAnswer: the LLM timeout path falls back to a deterministic "having trouble" reply and persists the turn before delivery — fail-safe, not fail-open. No change.
  • linear-slack match: '@mention' and joke-bot channel guard fail closed (no channel → no reply). Correct; untouched.

I made no file edits; the working tree is pristine (all generated persona.json/.test-build artifacts are gitignored and were cleaned).

The one required item (the persona.json vs. npm test CI failure) needs a human decision on which of the two fixes to apply, so I am not printing READY.

khaliqgant and others added 2 commits June 22, 2026 11:28
Adopt the server-side threading pattern from internal-agents x-reply-radar: post
a compact `:newspaper: *Hacker News* — N new match(es)` header, then post the
digest body with `replyTo: <header ref>` so the cloud orders it after the header
delivers and sets thread_ts itself — no parent-receipt round-trip. Keeps the
channel scannable (header only) with detail in-thread.

- summarize() now returns { header, body } so the header can be the parent.
- Idempotency: claim "seen" before posting (at-least-once concurrency guard,
  cloud#1990); once the header lands, never throw out of the post block (a retry
  would re-post a duplicate header) — release the claim + rethrow only while
  nothing has posted yet, otherwise log loudly.
- no-receipt (ts:'') on either post is still a loud failure (silent-drop guard).
- tests updated for the two-message threaded shape (header + replyTo body).

Typecheck clean; full suite 142/142 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guard against malformed/old-schema `hn-monitor:post` records in the Q&A path:
optional-chain postedAt/digest in the prompt context, default stories→[] and
title/url in the fallback titles, and sort with localeCompare on a string
fallback so a missing postedAt can't crash or misorder. (Gemini review, skills#80)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@agent-relay-code

Copy link
Copy Markdown
Contributor

pr-reviewer could not complete review for #85 in AgentWorkforce/agents.
The review harness exited with code 1.
No review was posted; this needs operator attention.

@agent-relay-code

Copy link
Copy Markdown
Contributor

ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed.

Review: PR #85feat/inbox-buddy-email-chat-bot

Summary of the change

This PR adds a new inbox-buddy agent (a Slack-channel chat bot that answers questions about your Gmail with multi-turn continuity), threads the hn-monitor digest under a count header, adds README banners + avatar/card/banner PNGs for several agents, and extends the eval harness (run-evals.mjs) to support multi-turn chat cases with new Gmail seed fixtures and eval cases.

What I verified

  • Canonical CI gate (npm test = tsc --outDir .test-build + node --test): after clearing the sandbox's leaked env vars, 142/142 tests pass.
  • tsc --noEmit typecheck: clean, no errors.
  • Evals (simulate mode) for the two new inbox-buddy cases: both PASS — confirming the run-evals envelope/multi-turn changes and the handler work end-to-end against the seed fixtures.
  • Impact trace: inbox-buddy's lib exports are consumed only by its own test file; no cross-package coupling, no downstream breakage. New daytona-monitor/fixtures/inbox-test.json is an unreferenced dev fixture (harmless). PNG/README changes are cosmetic.
  • Working tree left clean — no edits made (no mechanical fixes were warranted).

Important note on the apparent test failures

A first raw npm test showed 4 failures. I traced every one to this review sandbox's environment, not the PR code:

  • The pr-reviewer runtime exports SLACK_CHANNEL=C0ALQ06AAUT, WORKFORCE_SANDBOX_ROOT=/home/daytona/workspace, RELAYFILE_*, etc. into the process, and node --test inherits them.
  • inbox-buddy/hn-monitor resolve inputs via process.env[...] first (intentional: "env first for local dev"), and resolveMountRoot() honors WORKFORCE_SANDBOX_ROOT/WORKSPACE_ROOT above the test's RELAYFILE_MOUNT_ROOT. So the leaked vars hijacked channel resolution and the mount root.
  • Clearing those vars (env -u SLACK_CHANNEL -u WORKFORCE_SANDBOX_ROOT …) makes the full suite green. A clean CI runner has none of these set. No code fix is needed or appropriate here.

Addressed comments

  • Prior review rounds (commits "PR feat: conversational Slack chat bots (inbox-buddy + joke-bot) + @mention wake-gate #85 review round 2/3", round-3 "eval robustness + doc accuracy", hn-monitor "defensive reads"): validated against the current checkout — already incorporated in HEAD (defensive reads in loadPosts/handleInboxMessage, eval substring/grounding checks, replyContains machine checks, threading-under-header logic with rollback guards). Stale — already handled by later commits; nothing to re-apply.
  • No live bot/reviewer comment threads were provided in .workforce/ beyond the diff/context, and the PR shows no merge conflicts in the checkout.

Advisory Notes (out of scope for mechanical auto-fix — human decision)

  • Test env-isolation hardening (advisory, do not auto-apply): tests/inbox-buddy.test.mjs pins RELAYFILE_MOUNT_ROOT, but the runtime prefers WORKFORCE_SANDBOX_ROOT/WORKSPACE_ROOT/RELAYFILE_MOUNT_PATH, and several tests assume SLACK_CHANNEL is unset. Under an environment that sets those (as the cloud reviewer runtime does), the inbox-buddy/hn-monitor tests can spuriously fail. Consider making the tests neutralize the higher-precedence mount-root vars and SLACK_CHANNEL (e.g. save/clear/restore alongside the existing RELAYFILE_MOUNT_ROOT handling). I did not change tests — per policy, adding/altering tests to influence pass/fail is a human decision. Flagging only.

Verdict

The PR code is sound: it builds, typechecks, the full canonical test suite passes in a clean environment, and the new eval cases pass in simulate mode. I made no edits (no mechanical cleanup needed). The only open item is the advisory test-isolation note, which is a human decision and does not block.

I am not printing READY: I cannot confirm from this sandbox that all GitHub-required checks have completed and are passing (CI state is reported by cloud post-run), and the env-driven local failures, while explained, mean a human should confirm the real CI run is green before merge.

@khaliqgant khaliqgant merged commit 6d72d0a into main Jun 22, 2026
2 checks passed
@khaliqgant khaliqgant deleted the feat/inbox-buddy-email-chat-bot branch June 22, 2026 09:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant